Feature Gates Usage Guide
Feature gates (also known as feature toggles or feature tweaks) allow you to turn pieces of functionality on or off at runtime without shipping a new build. This guide explains what they are, when to use them, and how to implement a new tweak inside the Baselines architecture.
What Is a Feature Gate?
A feature gate is a small abstraction that tells the rest of the app whether a feature is currently
available. Under the hood it can talk to remote config, preferences, or any other store, but the
public API always looks the same: check if the feature is enabled, optionally flip its value, and
react in the UI based on the answer. In Baselines this contract lives in toolkit/feature-tweak module
and is modeled by:
AppFeatureenumFeatureTweakinterfaceTweaksfacade
When Are Feature Tweaks Needed?
Use a tweak whenever you want control over exposing a feature without republishing the app. Typical cases include:
- Gradually rolling out a capability to internal testers before going GA
- Keeping experimental UI behind a guard so it can be disabled quickly
- Building tooling (like the in-app Playground) where product or QA can toggle behaviors while exercising the app
If something must be safe to disable instantly—especially during early development—wrap it in a feature tweak.
Step-by-Step: Implement a New Tweak
Follow these three steps whenever you introduce a new gated feature. The snippets below assume we are
adding a Profile feature flag.
1️⃣ Extend FeatureTweak
Create a class that implements FeatureTweak for your feature. The implementation can inject any
dependencies it needs (config, storage, etc.) and do any kind of work to make enabled() / tweak(Boolean) functions
operate as needed. The code below automatically wires the tweak into the system, so the Tweaks class can pick it up.
kotlin1@Inject2@ContributesIntoMap(AppScope::class)3@FeatureKey(AppFeature.PROFILE)4// You can add `@SingleIn(AppScope::class)` to make it behave as a singleton,5// so it can store your runtime variables in memory, like `cachedState` below.6// In other cases `@SingleIn(AppScope::class)` is redundant.7@SingleIn(AppScope::class)8class ProfileFeatureTweak(9 private val appConfigManager: AppConfigManager,10) : FeatureTweak {1112 private var cachedState = true1314 override suspend fun enabled(): Boolean {15 val appInfo = appConfigManager.appConfig.first().info16 return appInfo.debug && cachedState17 }1819 override suspend fun tweak(enabled: Boolean) {20 cachedState = enabled21 }22}
2️⃣ Use Tweaks to read or flip the state
Inject Tweaks wherever the UI needs to react to the feature gate. ViewModels typically read the
value inside a mutableState block and invoke tweak() when the user flips a toggle. HomeViewModel
and the Playground FeatureTweakViewModel provide concrete examples.
kotlin1@Inject2@ContributesIntoMap(AppScope::class, binding<ViewModel>())3@ViewModelKey(ProfileViewModel::class)4class ProfileEntryViewModel(5 private val tweaks: Tweaks,6) : ViewModel(), Mvvm<ProfileUiEvent, ProfileUiState> {78 private val eventSink = createEventSink(::handleEvent)9 private val profileEnabledState = mutableState(false) {10 tweaks.enabled(AppFeature.PROFILE)11 }1213 @Composable14 override fun state(): ProfileUiState {15 val enabled by profileEnabledState.collectAsStateWithLifecycle()16 return ProfileUiState(17 enabled = enabled,18 eventSink = eventSink,19 )20 }2122 private fun handleEvent(event: ProfileUiEvent) {23 when (event) {24 ProfileUiEvent.ToggleProfile -> handleToggleProfile()25 }26 }2728 private fun handleToggleProfile() {29 launch {30 val enabled = profileEnabledState.value31 tweaks.tweak(AppFeature.PROFILE, !enabled)32 // If the current UI must reflect the updated tweak value,33 // recreate the state after applying the change.34 profileEnabledState.recreate()35 }36 }37}
With these pieces in place, the new feature gate becomes available throughout the app and can be controlled through the Playground tweaks UI or any other custom surface.